/*
 * Copyright (c) 2011 AndroidPlot.com. All rights reserved.
 *
 * Redistribution and use of source without modification and derived binaries with or without modification,
 * are permitted provided that the following conditions are met:
 *
 *    1. Redistributions of source code must retain the above copyright notice, this list of
 *       conditions and the following disclaimer.
 *
 *    2. Redistributions in binary form must reproduce the above copyright notice, this list
 *       of conditions and the following disclaimer in the documentation and/or other materials
 *       provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY ANDROIDPLOT.COM ``AS IS'' AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ANDROIDPLOT.COM OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * The views and conclusions contained in the software and documentation are those of the
 * authors and should not be interpreted as representing official policies, either expressed
 * or implied, of AndroidPlot.com.
 */

package com.androidplot.xy;

import android.content.Context;
//import android.graphics.*;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PointF;
import android.util.AttributeSet;
import com.androidplot.Plot;
import com.androidplot.series.XYSeries;
//import com.androidplot.xy.ui.widget.renderer.XYRendererType;
import com.androidplot.ui.*;
import com.androidplot.ui.widget.RangeLabelWidget;
import com.androidplot.ui.widget.DomainLabelWidget;
import com.androidplot.ui.TextOrientationType;
import com.androidplot.util.PixelUtils;

import java.text.Format;
import java.util.ArrayList;
import java.util.List;

/**
 * A View to graphically display x/y coordinates.
 */
public class XYPlot extends Plot<XYSeries, XYSeriesFormatter, XYSeriesRenderer> {

    private BoundaryMode domainOriginBoundaryMode;
    private BoundaryMode rangeOriginBoundaryMode;

    // widgets
    private XYLegendWidget legendWidget;
    private XYGraphWidget graphWidget;
    private DomainLabelWidget domainLabelWidget;
    private RangeLabelWidget rangeLabelWidget;

    private XYStepMode domainStepMode = XYStepMode.SUBDIVIDE;
    private double domainStepValue = 10;

    private XYStepMode rangeStepMode = XYStepMode.SUBDIVIDE;
    private double rangeStepValue = 10;

    private String domainLabel = "domain";
    private String rangeLabel = "range";

    // user settable min/max values
    private Number userMinX;
    private Number userMaxX;
    private Number userMinY;
    private Number userMaxY;

    // these are the final min/max used for dispplaying data
    private Number calculatedMinX;
    private Number calculatedMaxX;
    private Number calculatedMinY;
    private Number calculatedMaxY;

    // previous calculated min/max vals.
    // primarily used for GROW/SHRINK operations.
    private Number prevMinX;
    private Number prevMaxX;
    private Number prevMinY;
    private Number prevMaxY;

    // uses set boundary min and max values
    // should be null if not used.
    private Number rangeTopMin = null;
    private Number rangeTopMax = null;
    private Number rangeBottomMin = null;
    private Number rangeBottomMax = null;
    private Number domainLeftMin = null;
    private Number domainLeftMax = null;
    private Number domainRightMin = null;
    private Number domainRightMax = null;
    
    // used for  calculating the domain/range extents that will be displayed on the plot.
    // using boundaries and origins are mutually exclusive.  because of this,
    // setting one will disable the other.  when only setting the FramingModel,
    // the origin or boundary is set to the current value of the plot.
    private XYFramingModel domainFramingModel = XYFramingModel.EDGE;
    private XYFramingModel rangeFramingModel = XYFramingModel.EDGE;

    private Number userDomainOrigin;
    private Number userRangeOrigin;

    private Number calculatedDomainOrigin;
    private Number calculatedRangeOrigin;

    private Number domainOriginExtent = null;
    private Number rangeOriginExtent = null;

    private BoundaryMode domainUpperBoundaryMode = BoundaryMode.AUTO;
    private BoundaryMode domainLowerBoundaryMode = BoundaryMode.AUTO;
    private BoundaryMode rangeUpperBoundaryMode = BoundaryMode.AUTO;
    private BoundaryMode rangeLowerBoundaryMode = BoundaryMode.AUTO;

    private boolean drawDomainOriginEnabled = true;
    private boolean drawRangeOriginEnabled = true;

    private ArrayList<YValueMarker> yValueMarkers;
    private ArrayList<XValueMarker> xValueMarkers;

    private RectRegion defaultBounds;


    private static final int DEFAULT_LEGEND_WIDGET_H_DP             = 10;
    private static final int DEFAULT_LEGEND_WIDGET_ICON_SIZE_DP     = 7;
    private static final int DEFAULT_GRAPH_WIDGET_H_DP              = 18;
    private static final int DEFAULT_GRAPH_WIDGET_W_DP              = 10;
    private static final int DEFAULT_DOMAIN_LABEL_WIDGET_H_DP       = 10;
    private static final int DEFAULT_DOMAIN_LABEL_WIDGET_W_DP       = 80;
    private static final int DEFAULT_RANGE_LABEL_WIDGET_H_DP        = 50;
    private static final int DEFAULT_RANGE_LABEL_WIDGET_W_DP        = 10;

    private static final int DEFAULT_LEGEND_WIDGET_Y_OFFSET_DP       = 0;
    private static final int DEFAULT_LEGEND_WIDGET_X_OFFSET_DP       = 40;
    private static final int DEFAULT_GRAPH_WIDGET_Y_OFFSET_DP        = 0;
    private static final int DEFAULT_GRAPH_WIDGET_X_OFFSET_DP        = 0;
    private static final int DEFAULT_DOMAIN_LABEL_WIDGET_Y_OFFSET_DP = 0;
    private static final int DEFAULT_DOMAIN_LABEL_WIDGET_X_OFFSET_DP = 20;
    private static final int DEFAULT_RANGE_LABEL_WIDGET_Y_OFFSET_DP  = 0;
    private static final int DEFAULT_RANGE_LABEL_WIDGET_X_OFFSET_DP  = 0;

    private static final int DEFAULT_GRAPH_WIDGET_TOP_MARGIN_DP = 3;
    private static final int DEFAULT_GRAPH_WIDGET_RIGHT_MARGIN_DP = 3;
    private static final int DEFAULT_PLOT_LEFT_MARGIN_DP = 2;
    private static final int DEFAULT_PLOT_RIGHT_MARGIN_DP = 2;
    private static final int DEFAULT_PLOT_BOTTOM_MARGIN_DP = 2;

    {

    }


    public XYPlot(Context context, String title) {
        super(context, title);
        postInit(context);
    }

    public XYPlot(Context context, AttributeSet attributes) {
        super(context, attributes);
        postInit(context);
    }

    public XYPlot(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        postInit(context);
    }

    private void postInit(Context context) {
        legendWidget = new XYLegendWidget(
                this,
                new SizeMetrics(
                        PixelUtils.dpToPix(context, DEFAULT_LEGEND_WIDGET_H_DP),
                        SizeLayoutType.ABSOLUTE, 0.5f, SizeLayoutType.RELATIVE),
                new DynamicTableModel(0, 1),
                new SizeMetrics(
                        PixelUtils.dpToPix(context, DEFAULT_LEGEND_WIDGET_ICON_SIZE_DP),
                        SizeLayoutType.ABSOLUTE,
                        PixelUtils.dpToPix(context, DEFAULT_LEGEND_WIDGET_ICON_SIZE_DP),
                        SizeLayoutType.ABSOLUTE));

        graphWidget = new XYGraphWidget(
                this,
                new SizeMetrics(
                        PixelUtils.dpToPix(context, DEFAULT_GRAPH_WIDGET_H_DP),
                        SizeLayoutType.FILL,
                        PixelUtils.dpToPix(context, DEFAULT_GRAPH_WIDGET_W_DP),
                        SizeLayoutType.FILL));

        Paint backgroundPaint = new Paint();
        backgroundPaint.setColor(Color.DKGRAY);
        backgroundPaint.setStyle(Paint.Style.FILL);
        graphWidget.setBackgroundPaint(backgroundPaint);


        domainLabelWidget = new DomainLabelWidget(
                this,
                new SizeMetrics(
                        PixelUtils.dpToPix(context, DEFAULT_DOMAIN_LABEL_WIDGET_H_DP),
                        SizeLayoutType.ABSOLUTE,
                        PixelUtils.dpToPix(context, DEFAULT_DOMAIN_LABEL_WIDGET_W_DP),
                        SizeLayoutType.ABSOLUTE),
                TextOrientationType.HORIZONTAL);
        rangeLabelWidget = new RangeLabelWidget(
                this,
                new SizeMetrics(
                        PixelUtils.dpToPix(context, DEFAULT_RANGE_LABEL_WIDGET_H_DP),
                        SizeLayoutType.ABSOLUTE,
                        PixelUtils.dpToPix(context, DEFAULT_RANGE_LABEL_WIDGET_W_DP),
                        SizeLayoutType.ABSOLUTE),
                TextOrientationType.VERTICAL_ASCENDING);

        getLayoutManager().position(
                legendWidget,
                PixelUtils.dpToPix(context, DEFAULT_LEGEND_WIDGET_X_OFFSET_DP),
                XLayoutStyle.ABSOLUTE_FROM_RIGHT,
                PixelUtils.dpToPix(context, DEFAULT_LEGEND_WIDGET_Y_OFFSET_DP),
                YLayoutStyle.ABSOLUTE_FROM_BOTTOM,
                AnchorPosition.RIGHT_BOTTOM);

        getLayoutManager().position(
                graphWidget,
                PixelUtils.dpToPix(context, DEFAULT_GRAPH_WIDGET_X_OFFSET_DP),
                XLayoutStyle.ABSOLUTE_FROM_RIGHT,
                PixelUtils.dpToPix(context, DEFAULT_GRAPH_WIDGET_Y_OFFSET_DP),
                YLayoutStyle.ABSOLUTE_FROM_CENTER,
                AnchorPosition.RIGHT_MIDDLE);

        getLayoutManager().position(
                domainLabelWidget,
                PixelUtils.dpToPix(context, DEFAULT_DOMAIN_LABEL_WIDGET_X_OFFSET_DP),
                XLayoutStyle.ABSOLUTE_FROM_LEFT,
                PixelUtils.dpToPix(context, DEFAULT_DOMAIN_LABEL_WIDGET_Y_OFFSET_DP),
                YLayoutStyle.ABSOLUTE_FROM_BOTTOM,
                AnchorPosition.LEFT_BOTTOM);

        getLayoutManager().position(
                rangeLabelWidget,
                PixelUtils.dpToPix(context, DEFAULT_RANGE_LABEL_WIDGET_X_OFFSET_DP),
                XLayoutStyle.ABSOLUTE_FROM_LEFT,
                PixelUtils.dpToPix(context, DEFAULT_RANGE_LABEL_WIDGET_Y_OFFSET_DP),
                YLayoutStyle.ABSOLUTE_FROM_CENTER,
                AnchorPosition.LEFT_MIDDLE);

        getLayoutManager().moveToTop(getTitleWidget());
        getLayoutManager().moveToTop(getLegendWidget());
        graphWidget.setMarginTop(PixelUtils.dpToPix(context, DEFAULT_GRAPH_WIDGET_TOP_MARGIN_DP));
        graphWidget.setMarginRight(PixelUtils.dpToPix(context, DEFAULT_GRAPH_WIDGET_RIGHT_MARGIN_DP));

        getTitleWidget().pack();
        getDomainLabelWidget().pack();
        getRangeLabelWidget().pack();
        setPlotMarginLeft(PixelUtils.dpToPix(context, DEFAULT_PLOT_LEFT_MARGIN_DP));
        setPlotMarginRight(PixelUtils.dpToPix(context, DEFAULT_PLOT_RIGHT_MARGIN_DP));
        setPlotMarginBottom(PixelUtils.dpToPix(context, DEFAULT_PLOT_BOTTOM_MARGIN_DP));

        xValueMarkers = new ArrayList<XValueMarker>();
        yValueMarkers = new ArrayList<YValueMarker>();

        setDefaultBounds(new RectRegion(-1, 1, -1, 1));
    }


    public void setGridPadding(float left, float top, float right, float bottom) {
        getGraphWidget().setGridPaddingTop(top);
        getGraphWidget().setGridPaddingBottom(bottom);
        getGraphWidget().setGridPaddingLeft(left);
        getGraphWidget().setGridPaddingRight(right);
    }

    @Override
    protected XYSeriesRenderer doGetRendererInstance(Class clazz) {
        return XYRendererFactory.getInstance(this, clazz);
    }

    @Override
    protected void doBeforeDraw() {
        calculateMinMaxVals();
    }

    @Override
    protected void doAfterDraw() {

    }

    /**
     * Checks whether the point is within the plot's graph area.
     * @param x
     * @param y
     * @return
     */
    public boolean containsPoint(float x, float y) {
        if(getGraphWidget().getGridRect() != null) {
            return getGraphWidget().getGridRect().contains(x, y);
        }
        return false;
    }


    /**
     * Convenience method - wraps containsPoint(PointF).
     * @param point
     * @return
     */
    public boolean containsPoint(PointF point) {
        return containsPoint(point.x, point.y);
    }

    public void setCursorPosition(PointF point) {
        getGraphWidget().setCursorPosition(point);
    }

    public void setCursorPosition(float x, float y) {
        getGraphWidget().setCursorPosition(x, y);
    }

    public Number getYVal(PointF point) {
        throw new UnsupportedOperationException("Not yet implemented.");
        //ValPixConverter.pixToVal(point.y, )
    }

    public Number getXVal(PointF point) {
        throw new UnsupportedOperationException("Not yet implemented.");
    }

    private boolean isXValWithinView(double xVal) {
        return (userMinY == null || xVal >= userMinY.doubleValue()) &&
                userMaxY == null || xVal <= userMaxY.doubleValue();
    }

    private boolean isPointVisible(Number x, Number y) {
        // values without both an x and y val arent visible
        if( x == null || y == null) {
            return false;
        }
        return isValWithinRange(y.doubleValue(), userMinY, userMaxY) &&
                isValWithinRange(x.doubleValue(), userMinX, userMaxX);
    }

    private boolean isValWithinRange(double val, Number min, Number max) {
        boolean isAboveMinThreshold = min == null || val >= min.doubleValue();
        boolean isBelowMaxThreshold = max == null || val <= max.doubleValue();
        return isAboveMinThreshold &&
                isBelowMaxThreshold;
    }

    public void calculateMinMaxVals() {
        prevMinX = calculatedMinX;
        prevMaxX = calculatedMaxX;
        prevMinY = calculatedMinY;
        prevMaxY = calculatedMaxY;

        calculatedMinX = userMinX;
        calculatedMaxX = userMaxX;
        calculatedMinY = userMinY;
        calculatedMaxY = userMaxY;

        // next we go through each series to update our min/max values:
        for (XYSeries series : getSeriesSet()) {
            // step through each point in each series:
            for (int i = 0; i < series.size(); i++) {
                Number thisX = series.getX(i);
                Number thisY = series.getY(i);
                if (isPointVisible(thisX, thisY)) {
                    // only calculate if a static value has not been set:
                    if (userMinX == null) {
                        if (thisX != null && (calculatedMinX == null || thisX.doubleValue() < calculatedMinX.doubleValue())) {
                            calculatedMinX = thisX;
                        }
                    }

                    if (userMaxX == null) {
                        //Number thisMaxX = series.getMaxX();
                        if (thisX != null && (calculatedMaxX == null || thisX.doubleValue() > calculatedMaxX.doubleValue())) {
                            calculatedMaxX = thisX;
                        }
                    }

                    if (userMinY == null) {
                        //Number thisMinY = series.getMinY();
                        if (thisY != null && (calculatedMinY == null || thisY.doubleValue() < calculatedMinY.doubleValue())) {
                            calculatedMinY = thisY;
                        }
                    }

                    if (userMaxY == null) {
                        //Number thisMaxY = series.getMaxY();
                        if (thisY != null && (calculatedMaxY == null || thisY.doubleValue() > calculatedMaxY.doubleValue())) {
                            calculatedMaxY = thisY;
                        }
                    }
                }
            }
        }

        // at this point we now know what points are going to be visible on our
        // plot, but we still need to make corrections based on modes being used:
        // (grow, shrink etc.)
        switch(domainFramingModel) {
            case ORIGIN:
                updateDomainMinMaxForOriginModel();
                break;
            case EDGE:
                updateDomainMinMaxForEdgeModel();
                calculatedMinX = ApplyUserMinMax(calculatedMinX, domainLeftMin,
                        domainLeftMax);
                calculatedMaxX = ApplyUserMinMax(calculatedMaxX,
                        domainRightMin, domainRightMax);
                break;
            default:
                throw new UnsupportedOperationException("Domain Framing Model not yet supported: " + domainFramingModel);
        }

        switch(rangeFramingModel) {
            case ORIGIN:
                updateRangeMinMaxForOriginModel();
                break;
            case EDGE:
                updateRangeMinMaxForEdgeModel();
                calculatedMinY = ApplyUserMinMax(calculatedMinY,
                        rangeBottomMin, rangeBottomMax);
                calculatedMaxY = ApplyUserMinMax(calculatedMaxY, rangeTopMin,
                        rangeTopMax);
                break;
            default:
                throw new UnsupportedOperationException("Range Framing Model not yet supported: " + domainFramingModel);
        }

        calculatedDomainOrigin = userDomainOrigin != null ? userDomainOrigin : getCalculatedMinX();
        calculatedRangeOrigin = this.userRangeOrigin != null ? userRangeOrigin : getCalculatedMinY();
    }

    /**
     * Should ONLY be called from updateMinMax.
     * Results are undefined otherwise.
     */
    private void updateDomainMinMaxForEdgeModel() {
        switch(domainUpperBoundaryMode) {
            case FIXED:
                break;
            case AUTO:
                break;
            case GROW:
                if(!(prevMaxX == null || (calculatedMaxX.doubleValue() > prevMaxX.doubleValue()))) {
                    calculatedMaxX = prevMaxX;
                }
                break;
            case SHRINNK:
                if(!(prevMaxX == null || calculatedMaxX.doubleValue() < prevMaxX.doubleValue())) {
                    calculatedMaxX = prevMaxX;
                }
                break;
            default:
                throw new UnsupportedOperationException("DomainUpperBoundaryMode not yet implemented: " + domainUpperBoundaryMode);
        }

        switch (domainLowerBoundaryMode) {
            case FIXED:
                break;
            case AUTO:
                break;
            case GROW:
                if(!(prevMinX == null || calculatedMinX.doubleValue() < prevMinX.doubleValue())) {
                    calculatedMinX = prevMinX;
                }
                break;
            case SHRINNK:
                if(!(prevMinX == null || calculatedMinX.doubleValue() > prevMinX.doubleValue())) {
                    calculatedMinX = prevMinX;
                }
                break;
            default:
                throw new UnsupportedOperationException("DomainLowerBoundaryMode not supported: " + domainLowerBoundaryMode);
        }
    }

    public void updateRangeMinMaxForEdgeModel() {
        switch (rangeUpperBoundaryMode) {
            case FIXED:
                break;
            case AUTO:
                break;
            case GROW:
                if(!(prevMaxY == null || calculatedMaxY.doubleValue() > prevMaxY.doubleValue())) {
                    calculatedMaxY = prevMaxY;
                }
                break;
            case SHRINNK:
                if(!(prevMaxY == null || calculatedMaxY.doubleValue() < prevMaxY.doubleValue())) {
                   calculatedMaxY = prevMaxY;
                }
                break;
            default:
                throw new UnsupportedOperationException("RangeUpperBoundaryMode not supported: " + rangeUpperBoundaryMode);
        }

        switch (rangeLowerBoundaryMode) {
            case FIXED:
                break;
            case AUTO:
                break;
            case GROW:
                if(!(prevMinY == null || calculatedMinY.doubleValue() < prevMinY.doubleValue())) {
                    calculatedMinY = prevMinY;
                }
                break;
            case SHRINNK:
                if(!(prevMinY == null || calculatedMinY.doubleValue() > prevMinY.doubleValue())) {
                    calculatedMinY = prevMinY;
                }
                break;
            default:
                throw new UnsupportedOperationException("RangeLowerBoundaryMode not supported: " + rangeLowerBoundaryMode);
        }
    }

    /**
     * Apply user supplied min and max to the calculated boundary value.
     * 
     * @param value
     * @param min
     * @param max
     */
    private Number ApplyUserMinMax(Number value, Number min, Number max) {
        value = (((min == null) || (value.doubleValue() > min.doubleValue()))
                ? value
                : min);
        value = (((max == null) || (value.doubleValue() < max.doubleValue()))
                ? value
                : max);
        return value;
    }
    
    /**
     * Centers the domain axis on origin.
     * @param origin
     */
    public void centerOnDomainOrigin(Number origin) {
        centerOnDomainOrigin(origin, null, BoundaryMode.AUTO);
    }

    /**
     * Centers the domain on origin, calculating the upper and lower boundaries of the axis
     * using mode and extent.
     * @param origin
     * @param extent
     * @param mode
     */
    public void centerOnDomainOrigin(Number origin, Number extent, BoundaryMode mode) {
        if (origin == null) {
            throw new NullPointerException("Origin param cannot be null.");
        }
        domainFramingModel = XYFramingModel.ORIGIN;
        setUserDomainOrigin(origin);
        domainOriginExtent = extent;
        domainOriginBoundaryMode = mode;

        if (domainOriginBoundaryMode == BoundaryMode.FIXED) {
            double domO = userDomainOrigin.doubleValue();
            double domE = domainOriginExtent.doubleValue();
            userMaxX = domO + domE;
            userMinX = domO - domE;
        } else {
            userMaxX = null;
            userMinX = null;
        }
    }

    /**
     * Centers the range axis on origin.
     * @param origin
     */
    public void centerOnRangeOrigin(Number origin) {
        centerOnRangeOrigin(origin, null, BoundaryMode.AUTO);
    }

    /**
     * Centers the domain on origin, calculating the upper and lower boundaries of the axis
     * using mode and extent.
     * @param origin
     * @param extent
     * @param mode
     */
    public void centerOnRangeOrigin(Number origin, Number extent, BoundaryMode mode) {
        if (origin == null) {
            throw new NullPointerException("Origin param cannot be null.");
        }
        rangeFramingModel = XYFramingModel.ORIGIN;
        setUserRangeOrigin(origin);
        rangeOriginExtent = extent;
        rangeOriginBoundaryMode = mode;

        if (rangeOriginBoundaryMode == BoundaryMode.FIXED) {
            double raO = userRangeOrigin.doubleValue();
            double raE = rangeOriginExtent.doubleValue();
            userMaxY = raO + raE;
            userMinY = raO - raE;
        } else {
            userMaxY = null;
            userMinY = null;
        }
    }

    /**
     * Returns the distance between x and y.
     * Result is never a negative number.
     * @param x
     * @param y
     * @return
     */
    private double distance(double x, double y) {
        if(x > y) {
            return x - y;
        } else {
            return y - x;
        }
    }

    /*private double min(double a, double b) {
        if(a < b) {
            return a;
        } else {
            return b;
        }
    }*/

    /*private double max(double a, double b) {
        if(a > b) {
            return a;
        } else {
            return b;
        }
    }*/

    public void updateDomainMinMaxForOriginModel() {
        double origin = userDomainOrigin.doubleValue();
        double maxXDelta = distance(calculatedMaxX.doubleValue(), origin);
        double minXDelta = distance(calculatedMinX.doubleValue(), origin);
        double delta = maxXDelta > minXDelta ? maxXDelta : minXDelta;
        double dlb = origin - delta;
        double dub = origin + delta;
        switch (domainOriginBoundaryMode) {
            case AUTO:
                calculatedMinX = dlb;
                calculatedMaxX = dub;

                break;
            // if fixed, then the value already exists within "user" vals.
            case FIXED:
                break;
            case GROW: {

                if (prevMinX == null || dlb < prevMinX.doubleValue()) {
                    calculatedMinX = dlb;
                } else {
                    calculatedMinX = prevMinX;
                }

                if (prevMaxX == null || dub > prevMaxX.doubleValue()) {
                    calculatedMaxX = dub;
                } else {
                    calculatedMaxX = prevMaxX;
                }
            }
            break;
            case SHRINNK:
                if (prevMinX == null || dlb > prevMinX.doubleValue()) {
                    calculatedMinX = dlb;
                } else {
                    calculatedMinX = prevMinX;
                }

                if (prevMaxX == null || dub < prevMaxX.doubleValue()) {
                    calculatedMaxX = dub;
                } else {
                    calculatedMaxX = prevMaxX;
                }
                break;
            default:
                throw new UnsupportedOperationException("Domain Origin Boundary Mode not yet supported: " + domainOriginBoundaryMode);
        }
    }

    public void updateRangeMinMaxForOriginModel() {
        switch (rangeOriginBoundaryMode) {
            case AUTO:
                double origin = userRangeOrigin.doubleValue();
                double maxYDelta = distance(calculatedMaxY.doubleValue(), origin);
                double minYDelta = distance(calculatedMinY.doubleValue(), origin);
                if(maxYDelta > minYDelta) {
                    calculatedMinY = origin - maxYDelta;
                    calculatedMaxY = origin + maxYDelta;
                } else {
                    calculatedMinY = origin - minYDelta;
                    calculatedMaxY = origin + minYDelta;
                }
                break;
            case FIXED:
            case GROW:
            case SHRINNK:
            default:
                throw new UnsupportedOperationException("Range Origin Boundary Mode not yet supported: " + rangeOriginBoundaryMode);
        }
    }

    public boolean addSeries(XYSeries series, XYSeriesFormatter formatter) {
        Class clazz = XYRendererFactory.getRendererClass(formatter);
        return addSeries(series, clazz, formatter);
    }

    /*public boolean addSeries(XYSeries series, LineAndPointFormatter formatter) {
        return addSeries(series, LineAndPointRenderer.class, formatter);
    }

    public boolean addSeries(XYSeries series, BarFormatter formatter) {
        return addSeries(series, BarRenderer.class, formatter);
    }

    public boolean addSeries(XYSeries series, StepFormatter formatter) {
        return addSeries(series, StepRenderer.class, formatter);
    }*/
    
    /**
     * Convenience method - wraps XYGraphWidget.getTicksPerRangeLabel().
     * Equivalent to getGraphWidget().getTicksPerRangeLabel().
     * @return
     */
    public int getTicksPerRangeLabel() {
        return graphWidget.getTicksPerRangeLabel();
    }

    /**
     * Convenience method - wraps XYGraphWidget.setTicksPerRangeLabel().
     * Equivalent to getGraphWidget().setTicksPerRangeLabel().
     * @param ticksPerRangeLabel
     */
    public void setTicksPerRangeLabel(int ticksPerRangeLabel) {
       graphWidget.setTicksPerRangeLabel(ticksPerRangeLabel);
    }

    /**
     * Convenience method - wraps XYGraphWidget.getTicksPerDomainLabel().
     * Equivalent to getGraphWidget().getTicksPerDomainLabel().
     * @return
     */
    public int getTicksPerDomainLabel() {
        return graphWidget.getTicksPerDomainLabel();
    }

    /**
     * Convenience method - wraps XYGraphWidget.setTicksPerDomainLabel().
     * Equivalent to getGraphWidget().setTicksPerDomainLabel().
     * @param ticksPerDomainLabel
     */
    public void setTicksPerDomainLabel(int ticksPerDomainLabel) {
        graphWidget.setTicksPerDomainLabel(ticksPerDomainLabel);
    }

    public XYStepMode getDomainStepMode() {
        return domainStepMode;
    }

    public void setDomainStepMode(XYStepMode domainStepMode) {
        this.domainStepMode = domainStepMode;
    }

    public double getDomainStepValue() {
        return domainStepValue;
    }

    public void setDomainStepValue(double domainStepValue) {
        this.domainStepValue = domainStepValue;
    }

    public void setDomainStep(XYStepMode mode, double value) {
        setDomainStepMode(mode);
        setDomainStepValue(value);
    }

    public XYStepMode getRangeStepMode() {
        return rangeStepMode;
    }

    public void setRangeStepMode(XYStepMode rangeStepMode) {
        this.rangeStepMode = rangeStepMode;
    }

    public double getRangeStepValue() {
        return rangeStepValue;
    }

    public void setRangeStepValue(double rangeStepValue) {
        this.rangeStepValue = rangeStepValue;
    }

    public void setRangeStep(XYStepMode mode, double value) {
        setRangeStepMode(mode);
        setRangeStepValue(value);
    }

    public String getDomainLabel() {
        return domainLabel;
    }

    public void setDomainLabel(String domainLabel) {
        this.domainLabel = domainLabel;
        if(getDomainLabelWidget() != null) {
            getDomainLabelWidget().pack();
        }
    }

    public String getRangeLabel() {
        return rangeLabel;
    }

    public void setRangeLabel(String rangeLabel) {
        this.rangeLabel = rangeLabel;
        if(getRangeLabelWidget() != null) {
            getRangeLabelWidget().pack();
        }
    }

    public XYLegendWidget getLegendWidget() {
        return legendWidget;
    }

    public void setLegendWidget(XYLegendWidget legendWidget) {
        this.legendWidget = legendWidget;
    }

    public XYGraphWidget getGraphWidget() {
        return graphWidget;
    }

    public void setGraphWidget(XYGraphWidget graphWidget) {
        this.graphWidget = graphWidget;
    }

    public DomainLabelWidget getDomainLabelWidget() {
        return domainLabelWidget;
    }

    public void setDomainLabelWidget(DomainLabelWidget domainLabelWidget) {
        this.domainLabelWidget = domainLabelWidget;
    }

    public RangeLabelWidget getRangeLabelWidget() {
        return rangeLabelWidget;
    }

    public void setRangeLabelWidget(RangeLabelWidget rangeLabelWidget) {
        this.rangeLabelWidget = rangeLabelWidget;
    }

    /**
     * Convenience method - wraps XYGraphWidget.getRangeValueFormat().
     * @return
     */
    public Format getRangeValueFormat() {
        return graphWidget.getRangeValueFormat();
    }

    /**
     * Convenience method - wraps XYGraphWidget.setRangeValueFormat().
     * @param rangeValueFormat
     */
    public void setRangeValueFormat(Format rangeValueFormat) {
        graphWidget.setRangeValueFormat(rangeValueFormat);
    }

    /**
     * Convenience method - wraps XYGraphWidget.getDomainValueFormat().
     * @return
     */
    public Format getDomainValueFormat() {
        return graphWidget.getDomainValueFormat();
    }

    /**
     * Convenience method - wraps XYGraphWidget.setDomainValueFormat().
     * @param domainValueFormat
     */
    public void setDomainValueFormat(Format domainValueFormat) {
        graphWidget.setDomainValueFormat(domainValueFormat);
    }

    /**
     * Setup the boundary mode, boundary values only applicable in FIXED mode.
     * @param lowerBoundary
     * @param upperBoundary
     * @param mode
     */
    public synchronized void setDomainBoundaries(Number lowerBoundary, Number upperBoundary, BoundaryMode mode) {
        setDomainBoundaries(lowerBoundary, mode, upperBoundary, mode);
    }

    /**
     * Setup the boundary mode, boundary values only applicable in FIXED mode.
     * @param lowerBoundary
     * @param lowerBoundaryMode
     * @param upperBoundary
     * @param upperBoundaryMode
     */
    public synchronized void setDomainBoundaries(Number lowerBoundary, BoundaryMode lowerBoundaryMode, Number upperBoundary, BoundaryMode upperBoundaryMode) {
        setDomainLowerBoundary(lowerBoundary, lowerBoundaryMode);
        setDomainUpperBoundary(upperBoundary, upperBoundaryMode);
    }
    /**
     * Setup the boundary mode, boundary values only applicable in FIXED mode.
     * @param lowerBoundary
     * @param upperBoundary
     * @param mode
     */
    public synchronized void setRangeBoundaries(Number lowerBoundary, Number upperBoundary, BoundaryMode mode) {
        setRangeBoundaries(lowerBoundary, mode, upperBoundary, mode);
    }
    /**
     * Setup the boundary mode, boundary values only applicable in FIXED mode.
     * @param lowerBoundary
     * @param lowerBoundaryMode
     * @param upperBoundary
     * @param upperBoundaryMode
     */
    public synchronized void setRangeBoundaries(Number lowerBoundary, BoundaryMode lowerBoundaryMode, Number upperBoundary, BoundaryMode upperBoundaryMode) {
        setRangeLowerBoundary(lowerBoundary, lowerBoundaryMode);
        setRangeUpperBoundary(upperBoundary, upperBoundaryMode);
    }

    protected synchronized void setDomainUpperBoundaryMode(BoundaryMode mode) {
        this.domainUpperBoundaryMode = mode;
    }

    protected synchronized void setUserMaxX(Number boundary) {
        // Ifor 12/10/2011
        // you want null for auto grow and shrink
        //if(boundary == null) {
        //    throw new NullPointerException("Boundary value cannot be null.");
        //}
        this.userMaxX = boundary;
    }
    /**
     * Setup the boundary mode, boundary values only applicable in FIXED mode.
     * @param boundary
     * @param mode
     */
    public synchronized void setDomainUpperBoundary(Number boundary, BoundaryMode mode) {
        setUserMaxX((mode == BoundaryMode.FIXED) ? boundary : null);
        setDomainUpperBoundaryMode(mode);
        setDomainFramingModel(XYFramingModel.EDGE);
    }

    protected synchronized void setDomainLowerBoundaryMode(BoundaryMode mode) {
        this.domainLowerBoundaryMode = mode;
    }

    protected synchronized void setUserMinX(Number boundary) {
        // Ifor 12/10/2011
        // you want null for auto grow and shrink
        //if(boundary == null) {
        //    throw new NullPointerException("Boundary value cannot be null.");
        //}
        this.userMinX = boundary;
    }
    /**
     * Setup the boundary mode, boundary values only applicable in FIXED mode.
     * @param boundary
     * @param mode
     */
    public synchronized void setDomainLowerBoundary(Number boundary, BoundaryMode mode) {
        setUserMinX((mode == BoundaryMode.FIXED) ? boundary : null);
        setDomainLowerBoundaryMode(mode);
        setDomainFramingModel(XYFramingModel.EDGE);
        //updateMinMaxVals();
    }

    protected synchronized void setRangeUpperBoundaryMode(BoundaryMode mode) {
        this.rangeUpperBoundaryMode = mode;
    }

    protected synchronized void setUserMaxY(Number boundary) {
        // Ifor 12/10/2011
        // you want null for auto grow and shrink
        //if(boundary == null) {
        //    throw new NullPointerException("Boundary value cannot be null.");
        //}
        this.userMaxY = boundary;
    }
    /**
     * Setup the boundary mode, boundary values only applicable in FIXED mode.
     * @param boundary
     * @param mode
     */
    public synchronized void setRangeUpperBoundary(Number boundary, BoundaryMode mode) {
        setUserMaxY((mode == BoundaryMode.FIXED) ? boundary : null);
        setRangeUpperBoundaryMode(mode);
        setRangeFramingModel(XYFramingModel.EDGE);
        //updateMinMaxVals();
    }

    protected synchronized void setRangeLowerBoundaryMode(BoundaryMode mode) {
        this.rangeLowerBoundaryMode = mode;
    }

    protected synchronized void setUserMinY(Number boundary) {
        // Ifor 12/10/2011
        // you want null for auto grow and shrink
        //if(boundary == null) {
        //    throw new NullPointerException("Boundary value cannot be null.");
        //}
        this.userMinY = boundary;
    }
    /**
     * Setup the boundary mode, boundary values only applicable in FIXED mode.
     * @param boundary
     * @param mode
     */
    public synchronized void setRangeLowerBoundary(Number boundary, BoundaryMode mode) {
        setUserMinY((mode == BoundaryMode.FIXED) ? boundary : null);
        setRangeLowerBoundaryMode(mode);
        setRangeFramingModel(XYFramingModel.EDGE);
    }

    private Number getUserMinX() {
        return userMinX;
    }

    private Number getUserMaxX() {
        return userMaxX;
    }

    private Number getUserMinY() {
        return userMinY;
    }

    private Number getUserMaxY() {
        return userMaxY;
    }

    public Number getDomainOrigin() {
        return calculatedDomainOrigin;
    }

    public Number getRangeOrigin() {
        return calculatedRangeOrigin;
    }

    protected BoundaryMode getDomainUpperBoundaryMode() {
        return domainUpperBoundaryMode;
    }

    protected BoundaryMode getDomainLowerBoundaryMode() {
        return domainLowerBoundaryMode;
    }

    protected BoundaryMode getRangeUpperBoundaryMode() {
        return rangeUpperBoundaryMode;
    }

    protected BoundaryMode getRangeLowerBoundaryMode() {
        return rangeLowerBoundaryMode;
    }

    public synchronized void setUserDomainOrigin(Number origin) {
        if(origin == null) {
            throw new NullPointerException("Origin value cannot be null.");
        }
        this.userDomainOrigin = origin;
    }

    public synchronized void setUserRangeOrigin(Number origin) {
        if(origin == null) {
            throw new NullPointerException("Origin value cannot be null.");
        }
        this.userRangeOrigin = origin;
    }

    public XYFramingModel getDomainFramingModel() {
        return domainFramingModel;
    }

    protected void setDomainFramingModel(XYFramingModel domainFramingModel) {
        this.domainFramingModel = domainFramingModel;
    }

    public XYFramingModel getRangeFramingModel() {

        return rangeFramingModel;
    }

    protected void setRangeFramingModel(XYFramingModel rangeFramingModel) {
        this.rangeFramingModel = rangeFramingModel;
    }
    /**
     *  CalculatedMinX value after the the framing model has been applied.
     * @return
     */
    public Number getCalculatedMinX() {
        return calculatedMinX != null ? calculatedMinX : getDefaultBounds().getMinX();
    }

    /**
     *  CalculatedMaxX value after the the framing model has been applied.
     * @return
     */
    public Number getCalculatedMaxX() {
        return calculatedMaxX != null ? calculatedMaxX : getDefaultBounds().getMaxX();
    }

    /**
     *  CalculatedMinY value after the the framing model has been applied.
     * @return
     */
    public Number getCalculatedMinY() {
        return calculatedMinY != null ? calculatedMinY : getDefaultBounds().getMinY();
    }

    /**
     *  CalculatedMaxY value after the the framing model has been applied.
     * @return
     */
    public Number getCalculatedMaxY() {
        return calculatedMaxY != null ? calculatedMaxY : getDefaultBounds().getMaxY();
    }

    public boolean isDrawDomainOriginEnabled() {
        return drawDomainOriginEnabled;
    }

    public void setDrawDomainOriginEnabled(boolean drawDomainOriginEnabled) {
        this.drawDomainOriginEnabled = drawDomainOriginEnabled;
    }

    public boolean isDrawRangeOriginEnabled() {
        return drawRangeOriginEnabled;
    }

    public void setDrawRangeOriginEnabled(boolean drawRangeOriginEnabled) {
        this.drawRangeOriginEnabled = drawRangeOriginEnabled;
    }

    /**
     * Appends the specified marker to the end of plot's yValueMarkers list.
     * @param marker The YValueMarker to be added.
     * @return true if the object was successfully added, false otherwise.
     */
    public boolean addMarker(YValueMarker marker) {
        if(yValueMarkers.contains(marker)) {
            return false;
        } else {
            return yValueMarkers.add(marker);
        }
    }

    /**
     * Removes the specified marker from the plot.
     * @param marker
     * @return The YValueMarker removed if successfull,  null otherwise.
     */
    public YValueMarker removeMarker(YValueMarker marker) {
        int markerIndex = yValueMarkers.indexOf(marker);
        if(markerIndex == -1) {
            return null;
        } else {
            return yValueMarkers.remove(markerIndex);
        }
    }

    /**
     * Convenience method - combines removeYMarkers() and removeXMarkers().
     * @return
     */
    public int removeMarkers() {
        int removed = removeXMarkers();
        removed += removeYMarkers();
        return removed;
    }

    /**
     * Removes all YValueMarker instances from the plot.
     * @return
     */
    public int removeYMarkers() {
        int numMarkersRemoved = yValueMarkers.size();
        yValueMarkers.clear();
        return numMarkersRemoved;
    }

    /**
     * Appends the specified marker to the end of plot's xValueMarkers list.
     * @param marker The XValueMarker to be added.
     * @return true if the object was successfully added, false otherwise.
     */
    public boolean addMarker(XValueMarker marker) {
        return !xValueMarkers.contains(marker) && xValueMarkers.add(marker);
    }

    /**
     * Removes the specified marker from the plot.
     * @param marker
     * @return The XValueMarker removed if successfull,  null otherwise.
     */
    public XValueMarker removeMarker(XValueMarker marker) {
        int markerIndex = xValueMarkers.indexOf(marker);
        if(markerIndex == -1) {
            return null;
        } else {
            return xValueMarkers.remove(markerIndex);
        }
    }

    /**
     * Removes all XValueMarker instances from the plot.
     * @return
     */
    public int removeXMarkers() {
        int numMarkersRemoved = xValueMarkers.size();
        xValueMarkers.clear();
        return numMarkersRemoved;
    }

    protected List<YValueMarker> getYValueMarkers() {
        return yValueMarkers;
    }

    protected List<XValueMarker> getXValueMarkers() {
        return xValueMarkers;
    }

    public RectRegion getDefaultBounds() {
        return defaultBounds;
    }

    public void setDefaultBounds(RectRegion defaultBounds) {
        this.defaultBounds = defaultBounds;
    }

    /**
     * @return the rangeTopMin
     */
    public Number getRangeTopMin() {
        return rangeTopMin;
    }

    /**
     * @param rangeTopMin
     *            the rangeTopMin to set
     */
    public synchronized void setRangeTopMin(Number rangeTopMin) {
        this.rangeTopMin = rangeTopMin;
    }

    /**
     * @return the rangeTopMax
     */
    public Number getRangeTopMax() {
        return rangeTopMax;
    }

    /**
     * @param rangeTopMax
     *            the rangeTopMax to set
     */
    public synchronized void setRangeTopMax(Number rangeTopMax) {
        this.rangeTopMax = rangeTopMax;
    }

    /**
     * @return the rangeBottomMin
     */
    public Number getRangeBottomMin() {
        return rangeBottomMin;
    }

    /**
     * @param rangeBottomMin
     *            the rangeBottomMin to set
     */
    public synchronized void setRangeBottomMin(Number rangeBottomMin) {
        this.rangeBottomMin = rangeBottomMin;
    }

    /**
     * @return the rangeBottomMax
     */
    public Number getRangeBottomMax() {
        return rangeBottomMax;
    }

    /**
     * @param rangeBottomMax
     *            the rangeBottomMax to set
     */
    public synchronized void setRangeBottomMax(Number rangeBottomMax) {
        this.rangeBottomMax = rangeBottomMax;
    }

    /**
     * @return the domainLeftMin
     */
    public Number getDomainLeftMin() {
        return domainLeftMin;
    }

    /**
     * @param domainLeftMin
     *            the domainLeftMin to set
     */
    public synchronized void setDomainLeftMin(Number domainLeftMin) {
        this.domainLeftMin = domainLeftMin;
    }

    /**
     * @return the domainLeftMax
     */
    public Number getDomainLeftMax() {
        return domainLeftMax;
    }

    /**
     * @param domainLeftMax
     *            the domainLeftMax to set
     */
    public synchronized void setDomainLeftMax(Number domainLeftMax) {
        this.domainLeftMax = domainLeftMax;
    }

    /**
     * @return the domainRightMin
     */
    public Number getDomainRightMin() {
        return domainRightMin;
    }

    /**
     * @param domainRightMin
     *            the domainRightMin to set
     */
    public synchronized void setDomainRightMin(Number domainRightMin) {
        this.domainRightMin = domainRightMin;
    }

    /**
     * @return the domainRightMax
     */
    public Number getDomainRightMax() {
        return domainRightMax;
    }

    /**
     * @param domainRightMax
     *            the domainRightMax to set
     */
    public synchronized void setDomainRightMax(Number domainRightMax) {
        this.domainRightMax = domainRightMax;
    }
}
